# Copyright (c) 2025 Huawei Technologies Co., Ltd.
# openUBMC is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.
import os
from types import MappingProxyType, SimpleNamespace
from typing import Dict, Iterable, Tuple

from conan import ConanFile
from conan.errors import ConanInvalidConfiguration
from conan.tools.build import check_min_cppstd
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
from conan.tools.files import (
    apply_conandata_patches,
    copy,
    export_conandata_patches,
    replace_in_file,
    rm,
    rmdir,
)
from conan.tools.microsoft import is_msvc_static_runtime, msvc_runtime_flag
from conan.tools.scm import Git, Version

required_conan_version = ">=2.13.0"

def _build_options_matrix() -> Dict[str, Iterable]:
    return {
        "shared": [True, False],
        "fPIC": [True, False],
        "build_gmock": [True, False],
        "no_main": [True, False],
        "hide_symbols": [True, False],
        "debug_postfix": ["ANY"],
        "disable_pthreads": [True, False],
    }


def _default_option_values() -> Dict[str, object]:
    return {
        "shared": False,
        "fPIC": True,
        "build_gmock": True,
        "no_main": False,
        "hide_symbols": False,
        "debug_postfix": "d",
        "disable_pthreads": False,
    }


def _compiler_matrix() -> MappingProxyType:
    return MappingProxyType(
        {
            "14": {
                "Visual Studio": "15",
                "msvc": "191",
                "gcc": "7.3.1",
                "clang": "6",
                "apple-clang": "12",
            },
        }
    )


RECIPE_META = SimpleNamespace(
    name="gtest",
    description="Google's C++ test framework",
    license="BSD-3-Clause",
    url="https://github.com/conan-io/conan-center-index",
    homepage="https://github.com/google/googletest",
    topics=("testing", "google-testing", "unit-test"),
)

RECIPE_CONFIG = SimpleNamespace(
    options=_build_options_matrix(),
    defaults=_default_option_values(),
    cppstd=14,
    compiler_floor=_compiler_matrix(),
)


def _is_version_lower(lhs, rhs):
    lhs_parts = [int(part) for part in str(lhs).split(".") if part.isdigit()]
    rhs_parts = [int(part) for part in str(rhs).split(".") if part.isdigit()]
    length = min(len(lhs_parts), len(rhs_parts))
    return lhs_parts[:length] < rhs_parts[:length]


def _debug_suffix_supported(version, build_type):
    return Version(version) < "1.12.0" and str(build_type) == "Debug"


class GoogleTestRecipe(ConanFile):
    name = RECIPE_META.name
    description = RECIPE_META.description
    license = RECIPE_META.license
    url = RECIPE_META.url
    homepage = RECIPE_META.homepage
    topics = RECIPE_META.topics
    package_type = "library"
    settings = "os", "arch", "compiler", "build_type"
    options = RECIPE_CONFIG.options.copy()
    default_options = RECIPE_CONFIG.defaults.copy()
    extension_properties = {"compatibility_cppstd": False}

    @property
    def _cppstd_floor(self):
        return RECIPE_CONFIG.cppstd

    @property
    def _compiler_boundaries(self):
        return RECIPE_CONFIG.compiler_floor.get(str(self._cppstd_floor), {})

    def layout(self):
        cmake_layout(self, src_folder="src")

    def config_options(self):
        if self.settings.os == "Windows":
            self.options.rm_safe("fPIC")
        if not _debug_suffix_supported(self.version, self.settings.build_type):
            self.options.rm_safe("debug_postfix")

    def configure(self):
        if self.options.shared:
            self.options.rm_safe("fPIC")

    def package_id(self):
        del self.info.options.no_main

    def export_sources(self):
        export_conandata_patches(self)

    def validate(self):
        if self.options.shared and is_msvc_static_runtime(self):
            raise ConanInvalidConfiguration("gtest shared is not compatible with static vc runtime")

        declared_cppstd = self.settings.get_safe("compiler.cppstd")
        if declared_cppstd:
            check_min_cppstd(self, self._cppstd_floor)

        compiler_name = str(self.settings.compiler)
        compiler_floor = self._compiler_boundaries.get(compiler_name)
        if compiler_floor and _is_version_lower(str(self.settings.compiler.version), compiler_floor):
            raise ConanInvalidConfiguration(
                f"{self.ref} requires C++{self._cppstd_floor}, which your compiler does not support."
            )

    def source(self):
        source_info = self.conan_data["sources"][self.version]
        Git(self).fetch_commit(url=source_info["url"], commit=source_info["branch"])

    def generate(self):
        tc = CMakeToolchain(self)
        cmake_vars = {
            "BUILD_GMOCK": bool(self.options.build_gmock),
            "gtest_hide_internal_symbols": bool(self.options.hide_symbols),
            "gtest_disable_pthreads": bool(self.options.disable_pthreads),
        }
        runtime = self.settings.compiler.get_safe("runtime")
        if runtime:
            cmake_vars["gtest_force_shared_crt"] = "MD" in msvc_runtime_flag(self)
        for key, value in cmake_vars.items():
            tc.variables[key] = value
        tc.generate()

    def build(self):
        self._apply_vendor_customizations()
        self._cmake_workflow()

    def package(self):
        copy(self, "LICENSE", self.source_folder, os.path.join(self.package_folder, "licenses"))
        cmake = CMake(self)
        cmake.install()
        for directory in ("pkgconfig", "cmake"):
            rmdir(self, os.path.join(self.package_folder, "lib", directory))
        rm(self, "*.pdb", os.path.join(self.package_folder, "lib"))

    def package_info(self):
        self.cpp_info.set_property("cmake_find_mode", "both")
        self.cpp_info.set_property("cmake_file_name", "GTest")

        components = self.cpp_info.components
        component_recipes = [
            {
                "name": "libgtest",
                "cmake": ("GTest::gtest", ["GTest::GTest"]),
                "pkg": "gtest",
                "libs": ["gtest"],
                "requires": [],
                "enabled": True,
            },
            {
                "name": "gtest_main",
                "cmake": ("GTest::gtest_main", ["GTest::Main"]),
                "pkg": "gtest_main",
                "libs": ["gtest_main"],
                "requires": ["libgtest"],
                "enabled": not self.options.no_main,
            },
            {
                "name": "gmock",
                "cmake": ("GTest::gmock", []),
                "pkg": "gmock",
                "libs": ["gmock"],
                "requires": ["libgtest"],
                "enabled": bool(self.options.build_gmock),
            },
            {
                "name": "gmock_main",
                "cmake": ("GTest::gmock_main", []),
                "pkg": "gmock_main",
                "libs": ["gmock_main"],
                "requires": ["gmock"],
                "enabled": bool(self.options.build_gmock and not self.options.no_main),
            },
        ]

        for recipe in component_recipes:
            if not recipe["enabled"]:
                continue
            comp = components[recipe["name"]]
            cmake_target, cmake_aliases = recipe["cmake"]
            comp.set_property("cmake_target_name", cmake_target)
            if cmake_aliases:
                comp.set_property("cmake_target_aliases", cmake_aliases)
            comp.set_property("pkg_config_name", recipe["pkg"])
            comp.libs = recipe["libs"]
            if recipe["requires"]:
                comp.requires = recipe["requires"]

        gtest_comp = components["libgtest"]
        if self.settings.os in ["Linux", "FreeBSD"]:
            gtest_comp.system_libs.append("m")
            if not self.options.disable_pthreads:
                gtest_comp.system_libs.append("pthread")
        if self.settings.os == "Neutrino" and getattr(self.settings.os, "version", None) == "7.1":
            gtest_comp.system_libs.append("regex")
        if self.options.shared:
            gtest_comp.defines.append("GTEST_LINKED_AS_SHARED_LIBRARY=1")

        self.cpp_info.names["cmake_find_package"] = "GTest"
        self.cpp_info.names["cmake_find_package_multi"] = "GTest"
        gtest_comp.names["cmake_find_package"] = "gtest"
        gtest_comp.names["cmake_find_package_multi"] = "gtest"

    def _apply_vendor_customizations(self):
        apply_conandata_patches(self)
        internal_utils = os.path.join(self.source_folder, "googletest", "cmake", "internal_utils.cmake")
        replace_in_file(self, internal_utils, "-WX", "")

    def _cmake_workflow(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()